import { stripVTControlCharacters } from 'node:util'

import { jestConsoleContext, jestContext } from '@prisma/get-platform'
import { ensureDir } from 'fs-extra'
import { stdin } from 'mock-stdin'
import prompt from 'prompts'
import tempy from 'tempy'

import { ErrorArea, RustPanic } from '..'
import { sendPanic } from '../sendPanic'
import { wouldYouLikeToCreateANewIssue } from '../utils/getGitHubIssueUrl'
import { handlePanic } from '../utils/handlePanic'

const keys = {
  up: '\x1B\x5B\x41',
  down: '\x1B\x5B\x42',
  enter: '\x0D',
  space: '\x20',
}
const sendKeystrokes = async (io) => {
  io.send(keys.down)
  io.send(keys.enter)
  await delay(10)
}
// helper function for timing
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) // Mock stdin so we can send messages to the CLI

const testRootDir = tempy.directory()

// eslint-disable-next-line @typescript-eslint/unbound-method
const oldProcessCwd = process.cwd

const sendPanicTag = 'send-panic-failed'

jest.mock('../sendPanic', () => ({
  ...jest.requireActual('../sendPanic'),
  sendPanic: jest.fn().mockImplementation(() => Promise.reject(new Error(sendPanicTag))),
}))

jest.mock('../utils/getGitHubIssueUrl', () => ({
  ...jest.requireActual('../utils/getGitHubIssueUrl'),
  wouldYouLikeToCreateANewIssue: jest.fn().mockImplementation(() => Promise.resolve()),
}))

const ctx = jestContext.new().add(jestConsoleContext()).assemble()

function restoreEnvSnapshot(snapshot: NodeJS.ProcessEnv) {
  for (const key of Object.keys(process.env)) {
    if (!(key in snapshot)) {
      delete process.env[key]
    }
  }

  for (const [key, value] of Object.entries(snapshot)) {
    if (value === undefined) {
      delete process.env[key]
    } else {
      process.env[key] = value
    }
  }
}

describe('handlePanic', () => {
  // testing with env https://stackoverflow.com/a/48042799/1345244
  const OLD_ENV = { ...process.env }

  // mock for retrieving the database version
  const getDatabaseVersionSafe = () => Promise.resolve(undefined)

  beforeEach(async () => {
    jest.resetModules() // most important - it clears the cache
    jest.clearAllMocks()

    restoreEnvSnapshot(OLD_ENV)
    process.env.GITHUB_ACTIONS = 'true' // simulate CI environment
    process.cwd = () => testRootDir

    await ensureDir(testRootDir)
  })

  afterEach(() => {
    process.cwd = oldProcessCwd
    // await del(testRootDir, { force: true }) // Need force: true because `del` does not delete dirs outside the CWD
  })
  let io
  beforeAll(() => (io = stdin()))
  afterAll(() => {
    restoreEnvSnapshot(OLD_ENV) // restore old env
    io.restore()
  })

  const error = new RustPanic('Some error message!', '', undefined, ErrorArea.LIFT_CLI)
  const packageJsonVersion = '0.0.0'
  const enginesVersion = '734ab53bd8e2cadf18b8b71cb53bf2d2bed46517'
  const command = 'something-test'

  // Only works locally (not in CI)
  it.skip('ask to submit the panic error in interactive mode', async () => {
    const oldConsoleLog = console.log
    const logs: string[] = []
    console.log = (...args) => {
      logs.push(...args)
    }

    setTimeout(() => sendKeystrokes(io).then(), 5)

    try {
      await handlePanic({
        error,
        cliVersion: packageJsonVersion,
        enginesVersion: enginesVersion,
        command,
        getDatabaseVersionSafe,
      })
    } catch (e) {
      expect(stripVTControlCharacters(e.message)).toMatchSnapshot()
    }

    console.log = oldConsoleLog
    expect(stripVTControlCharacters(logs.join('\n'))).toMatchSnapshot()
  })

  it('no interactive mode in CI', async () => {
    try {
      await handlePanic({
        error,
        cliVersion: packageJsonVersion,
        enginesVersion,
        command,
        getDatabaseVersionSafe,
      })
    } catch (error) {
      error.schemaPath = 'Some Schema Path'
      expect(error).toMatchInlineSnapshot(`[RustPanic: Some error message!]`)
      expect(JSON.stringify(error)).toMatchInlineSnapshot(
        `"{"__typename":"RustPanic","rustStack":"","area":"LIFT_CLI","name":"RustPanic","schemaPath":"Some Schema Path"}"`,
      )
    }
  })

  it('when sendPanic fails, the user should be alerted by a reportFailedMessage', async () => {
    const cliVersion = 'test-cli-version'
    const enginesVersion = 'test-engine-version'
    const rustStackTrace = 'test-rustStack'
    const command = 'test-command'

    const mockExit = jest.spyOn(process, 'exit').mockImplementation()

    const rustPanic = new RustPanic(
      'test-message',
      rustStackTrace,
      'test-request',
      ErrorArea.LIFT_CLI, // area
      undefined, // introspectionUrl
    )

    prompt.inject(['y']) // submit report
    await handlePanic({
      error: rustPanic,
      cliVersion,
      enginesVersion,
      command,
      getDatabaseVersionSafe,
    })

    expect(sendPanic).toHaveBeenCalledTimes(1)
    expect(wouldYouLikeToCreateANewIssue).toHaveBeenCalledTimes(1)
    expect(stripVTControlCharacters(ctx.mocked['console.log'].mock.calls.join('\n'))).toMatchSnapshot()
    expect(stripVTControlCharacters(ctx.mocked['console.error'].mock.calls.join('\n'))).toMatch(
      new RegExp(`^Error report submission failed due to:?`),
    )
    expect(mockExit).toHaveBeenCalledWith(1)
  })
})
